import 'dart:async';
import 'dart:collection';

import 'package:sqliteasy_core/sqliteasy_core.dart';
import 'package:sqliteasy_core/utils/collection_util.dart';
import 'package:sqliteasy_core/utils/sqlite_util.dart';

import 'migration_container.dart';

part 'table_info.dart';
part 'sqlite_type.dart';

const sqliteasyMasterTableName = "sqliteasy_master_table";
const sqliteasyIdentityColumnName = "identity_hash";
const sqliteasyIdColumnName = "id";
const sqliteasyIdentityColumnDefaultValue = "1";

abstract class SQLiteasyDatabase {
  DatabaseConfiguration _configuration;
  SupportSQLiteDatabase _db;
  bool _initialized = false;
  int get version;
  String get identityHash;

  Future init(DatabaseConfiguration configuration) async {
    this._configuration = configuration;
    _db = await _configuration.openAdapter.onOpen(configuration, version, onUpgrade, onDowngrade);
    _initialized = true;
    await _checkIdentity();
    onCreate();
    final result = await validateSchema(_db);
    if (!result.isValid) {
      throw Exception(result.errorMsg);
    }
  }

  void onCreate() {}

  void onUpgrade(int oldVersion, int newVersion, SupportSQLiteDatabase db) async {
    bool migrated = false;
    if (_configuration != null) {
      List<Migration> migrations = _configuration.migrationContainer.findMigrationPath(
          oldVersion, newVersion);
      if (migrations != null) {
        // TODO:Callback preMigration
        for (Migration migration in migrations) {
          migration.migrate(db);
        }
        SchemaValidateResult result = await validateSchema(db);
        if (!result.isValid) {
          throw new Exception("Migration didn't properly handle: "
              + result.errorMsg);
        }
        // TODO:postMigration callback
        updateIdentityHash(db);
        migrated = true;
      }
    }
    if (!migrated) {
      if (_configuration != null
          && !_configuration.isMigrationRequired(oldVersion, newVersion)) {
        //mDelegate.dropAllTables(db);
        //mDelegate.createAllTables(db);
      } else {
        throw new Exception("A migration from ${oldVersion} to ${newVersion} was required but not found. Please provide the "
            + "necessary Migration path via "
            + "RoomDatabase.Builder.addMigration(Migration ...) or allow for "
            + "destructive migrations via one of the "
            + "RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");
      }
    }
  }

  void onDowngrade(int oldVersion, int newVersion, SupportSQLiteDatabase db) {
    onUpgrade(oldVersion, newVersion, db);
  }

  Future<SchemaValidateResult> validateSchema(SupportSQLiteDatabase db);

  @override
  bool isOpen() {
    _checkInitialized();
    return _db.isOpen();
  }

  @override
  void close() {
    return _db.close();
  }

  @override
  Future beginTransaction([bool exclusive = true]) async {
    _checkInitialized();
    await _db.beginTransaction(exclusive);
  }

  @override
  Future<int> delete(String sql, [List<dynamic> arguments]) async {
    _checkInitialized();
    await _db.delete(sql, arguments);
  }

  @override
  Future endTransaction() async {
    _checkInitialized();
    await _db.endTransaction();
  }

  @override
  Future<int> insert(String sql, [List<dynamic> arguments]) async {
    _checkInitialized();
    return await _db.insert(sql, arguments);
  }

  @override
  Future query(String sql, [List<dynamic> arguments]) async {
    _checkInitialized();
    return await _db.query(sql, arguments);
  }

  @override
  Future runInTransaction(FutureOr Function() block, [bool exclusive = true]) async {
    _checkInitialized();
    await _db.runInTransaction(block, exclusive);
  }

  @override
  Future setTransactionSuccessful() async {
    _checkInitialized();
    return await _db.setTransactionSuccessful();
  }

  @override
  Future exec(String sql, [List arguments]) async {
    _checkInitialized();
    return await _db.exec(sql, arguments);
  }

  Future update(String tableName, Map<String, Object> columnMap, Map<String, Object> primaryKeyMap,
      [NullValueStrategy nullValueStrategy = NullValueStrategy.apply]) async {
    _checkInitialized();
    return await _db.update(tableName, columnMap, primaryKeyMap, nullValueStrategy);
  }

  void _checkInitialized() {
    if (!_initialized) {
      throw Exception("you must call the init method of ${runtimeType} before using it.");
    }
  }

  void _checkIdentity() async {
    if (await SQLiteUtil.hasSqliteasyMasterTable(_db)) {
      final result = await exec("select ${sqliteasyIdentityColumnName} from ${sqliteasyMasterTableName} where id=${sqliteasyIdentityColumnDefaultValue} limit 1");
      final identityHash = await result[0][sqliteasyIdentityColumnName];
      if (identityHash != this.identityHash) {
        throw Exception("SQLiteasy cannot verify the data integrity. Looks like"
            + " you've changed schema but forgot to update the version number. You can"
            + " simply fix this by increasing the version number.");
      }
    } else {
      // No sqliteasy_master_table, this might an a pre-populated DB, we must validate to see if
      // its suitable for usage.
      if (await _hasEmptySchema(_db)) {
        return;
      }
      SchemaValidateResult result = await validateSchema(_db);
      if (!result.isValid) {
        throw Exception("Pre-packaged database has an invalid schema: "
            + result.errorMsg);
      }
      updateIdentityHash(_db);
    }
  }

  static Future<bool> _hasEmptySchema(SupportSQLiteDatabase db) async {
    List result = await db.query(
        "SELECT count(*) as count FROM sqlite_master WHERE name != 'android_metadata'");
    //noinspection TryFinallyCanBeTryWithResources
    return result[0]['count'] == 0;
  }

  void updateIdentityHash(SupportSQLiteDatabase db) {
    db.exec("INSERT OR REPLACE INTO sqliteasy_master_table (id, identity_hash) VALUES(1, '${identityHash}')");
  }
}

class SchemaValidateResult {
  final bool isValid;
  final String errorMsg;

  SchemaValidateResult({this.isValid, this.errorMsg});
}

abstract class SupportSQLiteDatabase {
  bool isOpen();

  void close();

  Future beginTransaction([bool exclusive = true]);

  Future endTransaction();

  Future setTransactionSuccessful();

  Future runInTransaction(FutureOr Function() block, [bool exclusive = true]);

  /// Executes a raw SQL INSERT query and returns the last inserted row ID.
  ///
  /// ```
  /// int id1 = await database.rawInsert(
  ///   'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
  /// ```
  Future<int> insert(String sql, [List<dynamic> arguments]);

  /// Executes a raw SQL SELECT query and returns a list
  /// of the rows that were found.
  ///
  /// ```
  /// List<Map> list = await database.rawQuery('SELECT * FROM Test');
  /// ```
  Future query(String sql, [List<dynamic> arguments]);

  /// Executes a raw SQL UPDATE query and returns
  /// the number of changes made.
  ///
  /// ```
  /// int count = await database.rawUpdate(
  ///   'UPDATE Test SET name = ?, value = ? WHERE name = ?',
  ///   ['updated name', '9876', 'some name']);
  /// ```
  Future<int> update(String tableName, Map<String, Object> columnMap, Map<String, Object> primaryKeyMap,
      [NullValueStrategy nullValueStrategy = NullValueStrategy.apply]);

  /// Executes a raw SQL DELETE query and returns the
  /// number of changes made.
  ///
  /// ```
  /// int count = await database
  ///   .rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
  /// ```
  Future<int> delete(String sql, [List<dynamic> arguments]);

  Future exec(String sql, [List<dynamic> arguments]);
}

class SqlStatement {
  int _parameterCount;
  bool _paramIsEntity;
  String statement;
  DaoMethodType methodType;
  List<SQLiteValueType> _bindingTypeMap = List();
  List<String> _valueFieldMap = List();
  List<String> _columnNameMap = List();
  NullValueStrategy nullValueStrategy;

  SqlStatement(this.statement, this.methodType, [this._paramIsEntity = true]) {
    _parameterCount = this.statement.length - this.statement.replaceAll("?", "").length;
    _bindingTypeMap = List(_parameterCount + 1);
    _valueFieldMap = List(_parameterCount + 1);
    _columnNameMap = List(_parameterCount + 1);
  }

  void bindValueField(int index, String fieldName) {
    _valueFieldMap[index] = fieldName;
  }

  void bindColumnName(int index, String columnName) {
    _columnNameMap[index] = columnName;
  }

  String getFieldNameAt(int index) {
    return _valueFieldMap[index];
  }

  String getColumnNameAt(int index) {
    return _columnNameMap[index];
  }

  int get parameterCount {
    return _parameterCount;
  }

  bool get paramIsEntity {
    return _paramIsEntity;
  }
}

class DatabaseConfiguration {
  final String name;
  final DatabaseOpenAdapter openAdapter;
  final MigrationContainer migrationContainer;
  final bool requireMigration;
  final bool allowDestructiveMigrationOnDowngrade;
  Set<int> _migrationNotRequiredFrom;

  DatabaseConfiguration(this.name, this.openAdapter,
      {MigrationContainer migrationContainer,
      this.requireMigration = true,
      this.allowDestructiveMigrationOnDowngrade = false,
      Set<int> migrationNotRequiredFrom})
      : this._migrationNotRequiredFrom = migrationNotRequiredFrom,
        this.migrationContainer = migrationContainer ?? MigrationContainer();

  bool isMigrationRequired(int fromVersion, int toVersion) {
    // Migrations are not required if its a downgrade AND destructive migration during downgrade
    // has been allowed.
    final bool isDowngrade = fromVersion > toVersion;
    if (isDowngrade && allowDestructiveMigrationOnDowngrade) {
      return false;
    }

    // Migrations are required between the two versions if we generally require migrations
    // AND EITHER there are no exceptions OR the supplied fromVersion is not one of the
    // exceptions.
    return requireMigration
        && (_migrationNotRequiredFrom == null
            || !_migrationNotRequiredFrom.contains(fromVersion));
  }
}

typedef VersionUpdater = FutureOr<void> Function(int oldVersion, int newVersion, SupportSQLiteDatabase db);
abstract class DatabaseOpenAdapter {
  Future<SupportSQLiteDatabase> onOpen(DatabaseConfiguration configuration, int version, VersionUpdater onUpgrade, VersionUpdater onDowngrade);
}

enum SQLiteValueType { Integer, Text, Blob, Real, Null }

enum DaoMethodType { Insert, Update, Delete, Query }

